Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例

概要

前面对"独占锁"和"共享锁"有了个大致的了解;和ReadWriteLock.ReadLock一样,CountDownLatch的本质也是一个"共享锁"。

 

CountDownLatch简介

CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。

CountDownLatch和CyclicBarrier的区别
(01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。
(02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
关于CyclicBarrier的原理,后面一章再来学习。

 

示例

CountDownLatch我理解是一个有多道锁的门闩,CountDownLatch在创建的时候就指定好有多少道锁链了。具体是这样的,假如有个门闩 CountDownLatch latch = new CountDownLatch(5),则这个门闩上面有5道锁链,当latch == 0 的时候说明门闩上5道锁都被解开了,这时候门闩打开了。

当线程调用latch.await方法的时候,会去检查latch内锁的数量是否等于0,也就是门闩是否打开了。

如果等于0说明门闩打开了,则不会被阻塞调用线程,直接运行后面的逻辑;如果 latch > 0 比如latch == 2 说明门闩上面还有2道锁,没打开,这个时候调用latch.await方法的线程就会被阻塞。

每次调用latch.countDown方法的时候,就会去掉一道锁,所以上面latch为5的时候需要调用5次countDown方法才能去掉门闩上所有的锁,让门闩打开。

示例说明:

public class CountDownLatchDemo {
    // 创建一个有2道锁的门闩
    public static CountDownLatch latch = new CountDownLatch(2);
    
    // 等待线程,等待门闩打开
    public static class WaitLatch extends Thread {
        @Override
        public void run() {
            try {
                // 等待门闩打开
                System.out.println(Thread.currentThread().getName() + "被门闩卡住了");
                latch.await();
                // 门闩打开的时候打印一下信息
                System.out.println("门闩打开啦," + Thread.currentThread().getName() + "通过啦");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    // 调用countDown减少门闩锁的线程
    public static class DownThread extends Thread {
        @Override
        public void run() {
            try {
                // 等3秒再去减少,让上面的WaitLatch线程先等着
                Thread.sleep(3000);
                // 减少门闩锁
                latch.countDown();
                System.out.println("释放门闩锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 两个等待latch打开的线程
        WaitLatch wait1 = new WaitLatch();
        WaitLatch wait2 = new WaitLatch();

        // 两个去减少latch的线程
        DownThread down1 = new DownThread();
        DownThread down2 = new DownThread();

        wait1.start();
        wait2.start();
        down1.start();
        down2.start();

        // 等待wait1、2,down1、down2线程运行结束之后,main线程再继续执行
        wait1.join();
        wait2.join();
        down1.join();
        down2.join();

        System.out.println("运行结束");
    }
}

 

 


上面的代码流程可以画一个图出来: 

 

最开始的时候wait1、wait2线程调用门闩的await方法,这个时候由于门闩上面还有2道锁,所以wait1、wait2被门闩卡住了,进入等待队列,阻塞等待门闩打开。
然后down1、down2线程分别调用countDown方法各自去掉门闩的一道锁,同时检查如果门闩上没锁了,则唤醒之前被门闩卡住的线程,让他们继续运行。

CountDownLatch的原理大致上就是这样子的,但是你知道它底层源码是怎么实现的吗?
那我就带你来分析分析CountDownLatch的底层源码

02 CountDownLatch底层源码分析

 CountDownLatch有一个内部类Sync,继承自AQS,重写了AQS的tryAcquireShared、tryReleaseShared方法,是一个共享锁。而CountDownLatch只是基于内部的Sync做了一层薄薄的封装而已。

 

 那Sync实现AQS的tryAcquireShared、tryReleaseShared的逻辑是什么呢?CountDownLatch又是基于Sync之上怎么封装的?

这个我们马上就来说,我们先来一个一个看。
CountDownLatch构造方法

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

上面的构造方法,内部其实就是创建一个Sync同步器,同时指定Sync内部的资源state == count,这里的state其实就是门闩上锁的数量。
await方法

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

内部直接调用AQS的acquireSharedInterruptibly方法,我们继续追踪:

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // AQS这里调用子类的tryAcquireShared方法
    // 如果返回结果大于0,继续执行业务代码
    // 如果返回结果小于0,则调用doAcquireSharedInterruptibly进入AQS等待队列阻塞等待
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

看到这里,其实就清晰了,进入到AQS的acquireSharedInterruptibly方法获取一个锁,之前我们讲过AQS的acquireShared方法,跟这里是一样的,是一个模板方法。
首先第一个调用的就是子类Sync的tryAcquireShard方法去获取资源。
如果获取成功就返回了,继续执行业务代码。
如果获取失败了就调用doAcquireShardInterruptibly方法进入AQS等待队列进行等待,这里的doAcquireSharedInterruptibly的内部逻辑跟我们之前分析的doAcquireShared是一样的。

说到这里,就只剩下Sync的tryAcquireShared方法逻辑了,我们继续分析:
Sync的tryAcquireShared方法

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

 


这里非常的简单,就是state == 0的时候返回1,其它情况均返回-1。

那这么说来,搞来搞去原来调用CountDownLatch的await方法是不是会被阻塞其实还是看Sync的tryAcquireShared方法是否返回1,也就是state 是否等于0咯?

没错,上面你不是说CountDownLatch是门闩嘛,其实state == 0就代表门闩上的锁都去掉了,所以门闩就打开了。
针对上面的await方法内部的逻辑和流程,我再给你画个图总结一下:

 

 上面CountDownLatch的await方法内部的大致流程图就是这样了,你看明白了没?
大致流程是这样的吧:
调用CountDownLatch的await方法其实进入的是AQS的内部acquireSharedInterruptibly方法,这个是AQS内部定义的模板流程方法。
首先就是调用子类Sync的tryAcquireShared方法,也就是实际判断门闩是否打开,当state  == 0 表示门闩上没锁了,打开。
当state != 0 表示门闩上还有锁,这个时候就需要进入AQS的等待队列进行等待咯,等待门闩打开后将线程唤醒。
这里CountDownLacth的await方法内部的源码和流程,就讲到这里咯。我们接下来继续,讲解CountDownLatch的countDown方法:
CountDownLatch的countDown方法

public void countDown() {
    sync.releaseShared(1);
}

这里的countDown方法直接就是调用AQS的releaseShared(1)方法,继续进入releaseShared方法

public final boolean releaseShared(int arg) {
    // 调用子类的tryReleaseShared方法,释放锁
    if (tryReleaseShared(arg)) {
        // 如果锁完全释放了,就唤醒等待队列中沉睡的线程
        doReleaseShared();
        return true;
    }
    return false;
}

这里就是进入AQS释放共享锁的模板流程了:
首先就是调用子类的tryReleaseShared方法释放锁,如果完全释放了,也就是state == 0 的时候,就调用AQS的doReleaseShared方法唤醒等待队列中等待的线程。
我们看看子类Sync的tryReleaseShared的逻辑
Sync的tryReleaseShared

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        // 如果state == 0,也就是锁的数量等于0,表示门闩打开了
        if (c == 0)
            return false;
         // 这里就是将state - 1,也就是将门闩上锁的数量减少一道
        int nextc = c-1;
        // CAS操作重新设置锁的数量
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

上面的代码非常简单,也就是将state的值减少1而已,表示将锁的数量减少一道。countDown内部的核心逻辑其实就是将state的数量减少1,也就是锁的数量减少1,当state == 0的时候,表示门闩已经打开,就可以调用doReleaseShared方法将AQS等待队列的线程唤醒了,表示:嘿,兄弟们,别睡了,门闩打开了。再加上doReleaseShared唤醒等待队列中的线程的源码,之前讲解AQS的共享锁机制的时候已经深入分析过了,所以这里我完全没有问题。  
从整体上画一个CountDownLatch的await方法、countDown方法的整体流程图:

 

 上面的这个图就是整体的CountDownLatch的await和countDown整体的流程以及交互的机制了,这下子CountDownLatch没问题了吧?
其实大概就是这样,最开始CountDownLatch latch = new CountDownLatch(2)的时候就是设置latch门闩上有2道锁。
然后线程A调用latch.await的时候发现上面还有锁,于是进入AQS等待队列睡觉去了。
线程B、C调用countDown方法,各自将锁减少一道,发现锁已经完全解开了,于是就是就唤醒了AQS中由于调用await方法陷入等待的线程。
这玩意,感觉就是一个计数开关一样,当门闩上锁为0的时候,开关打开,其它时候关闭,就是这么简单;只不过这东西是整合了AQS,利用了AQS的等待队列阻塞等待,以及唤醒机制而已。


CountDownLatch函数列表

复制代码
CountDownLatch(int count)
构造一个用给定计数初始化的 CountDownLatch。

// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
void await()
// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
boolean await(long timeout, TimeUnit unit)
// 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
void countDown()
// 返回当前计数。
long getCount()
// 返回标识此锁存器及其状态的字符串。
String toString()
复制代码

 

CountDownLatch数据结构

CountDownLatch的UML类图如下:

CountDownLatch的数据结构很简单,它是通过"共享锁"实现的。它包含了sync对象,sync是Sync类型。Sync是实例类,它继承于AQS。

 

CountDownLatch的使用示例

下面通过CountDownLatch实现:"主线程"等待"5个子线程"全部都完成"指定的工作(休眠1000ms)"之后,再继续运行。

复制代码
 1 import java.util.concurrent.CountDownLatch;
 2 import java.util.concurrent.CyclicBarrier;
 3 
 4 public class CountDownLatchTest1 {
 5 
 6     private static int LATCH_SIZE = 5;
 7     private static CountDownLatch doneSignal;
 8     public static void main(String[] args) {
 9 
10         try {
11             doneSignal = new CountDownLatch(LATCH_SIZE);
12 
13             // 新建5个任务
14             for(int i=0; i<LATCH_SIZE; i++)
15                 new InnerThread().start();
16 
17             System.out.println("main await begin.");
18             // "主线程"等待线程池中5个任务的完成
19             doneSignal.await();
20 
21             System.out.println("main await finished.");
22         } catch (InterruptedException e) {
23             e.printStackTrace();
24         }
25     }
26 
27     static class InnerThread extends Thread{
28         public void run() {
29             try {
30                 Thread.sleep(1000);
31                 System.out.println(Thread.currentThread().getName() + " sleep 1000ms.");
32                 // 将CountDownLatch的数值减1
33                 doneSignal.countDown();
34             } catch (InterruptedException e) {
35                 e.printStackTrace();
36             }
37         }
38     }
39 }
复制代码

运行结果

复制代码
main await begin.
Thread-0 sleep 1000ms.
Thread-2 sleep 1000ms.
Thread-1 sleep 1000ms.
Thread-4 sleep 1000ms.
Thread-3 sleep 1000ms.
main await finished.
复制代码

结果说明:主线程通过doneSignal.await()等待其它线程将doneSignal递减至0。其它的5个InnerThread线程,每一个都通过doneSignal.countDown()将doneSignal的值减1;当doneSignal为0时,main被唤醒后继续执行。

posted on 2016-11-14 22:13  duanxz  阅读(550)  评论(0编辑  收藏  举报